/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.internal.injection;

import java.io.*;
import ch.msoftch.asm.*;

/**
 * Rudimentary class file parser which figures out the class name, super class name and if a specified marker interface or {@link Serializable} is implemented.
 * 
 * @author M. Hautle
 */
public class ClassAnalyzer {
    /** The internal name of the {@link Serializable} interface */
    private static final String SERIALIZALBE_INTERFACE = Type.getInternalName(Serializable.class);

    /**
     * The type of CONSTANT_Class constant pool items.
     */
    private static final int CLASS = 7;

    /**
     * The type of CONSTANT_Fieldref constant pool items.
     */
    private static final int FIELD = 9;

    /**
     * The type of CONSTANT_Methodref constant pool items.
     */
    private static final int METH = 10;

    /**
     * The type of CONSTANT_InterfaceMethodref constant pool items.
     */
    private static final int IMETH = 11;

    /**
     * The type of CONSTANT_String constant pool items.
     */
    private static final int STR = 8;

    /**
     * The type of CONSTANT_Integer constant pool items.
     */
    private static final int INT = 3;

    /**
     * The type of CONSTANT_Float constant pool items.
     */
    private static final int FLOAT = 4;

    /**
     * The type of CONSTANT_Long constant pool items.
     */
    private static final int LONG = 5;

    /**
     * The type of CONSTANT_Double constant pool items.
     */
    private static final int DOUBLE = 6;

    /**
     * The type of CONSTANT_NameAndType constant pool items.
     */
    private static final int NAME_TYPE = 12;

    /**
     * The type of CONSTANT_Utf8 constant pool items.
     */
    private static final int UTF8 = 1;

    /** Access flag for interfaces. */
    private static final int INTERFACE = 0x0200;

    /** The internal name of the marker interface */
    private final String markerInterface;

    /**
     * Default constructor.
     * 
     * @param markerInterface The marker interface
     */
    public ClassAnalyzer(Class markerInterface) {
        this.markerInterface = Type.getInternalName(markerInterface);
    }

    /**
     * Default constructor.
     * 
     * @param markerInterface The marker interface name
     */
    public ClassAnalyzer(String markerInterface) {
        this.markerInterface = markerInterface.replace('.', '/');
    }

    /**
     * Analyzes the given class
     * 
     * @param is Inputstream of the class file (this method closes it after it has finished)
     * @return A {@link ClassInformation} Object containing the extracted informations
     * @throws IOException If something goes wrong
     */
    public ClassInformation analyze(final InputStream is) throws IOException {
        final ClassInformation result = new ClassInformation();
        try {
            // jump to constant pool count
            is.skip(8);
            final int cpCnt = readUnsignedShort(is);
            final String[] cpStrings = new String[cpCnt];
            final int[] cpPointers = new int[cpCnt];
            // read the strings and class infos of the constant pool
            for (int i = 1; i < cpPointers.length; i++) {
                final int v = is.read();
                switch (v) {
                    case UTF8:
                        cpStrings[i] = readUTF(is);
                        break;
                    case CLASS:
                        cpPointers[i] = readUnsignedShort(is);
                        break;
                    // skip the others
                    // 2 byte entries
                    case STR:
                        is.skip(2);
                        break;
                    // 4 bytes entries
                    case FIELD:
                    case METH:
                    case IMETH:
                    case INT:
                    case FLOAT:
                    case NAME_TYPE:
                        is.skip(4);
                        break;
                    // 8 byte entries
                    // using two slots in the constant pool
                    case LONG:
                    case DOUBLE:
                        is.skip(8);
                        i++;
                        break;
                    default:
                        throw new RuntimeException("Unknown Type " + v);
                }
            }
            // read access flags
            final int accessFlags = readUnsignedShort(is);
            // read class name
            result.internalName = cpStrings[cpPointers[readUnsignedShort(is)]];
            // read superclass name
            result.parentName = cpStrings[cpPointers[readUnsignedShort(is)]];
            // parse interface informations (if this is not an interface itself)
            final int intCnt = (accessFlags & INTERFACE) == 0 ? readUnsignedShort(is) : 0;
            for (int i = 0; i < intCnt; i++) {
                final String intName = cpStrings[cpPointers[readUnsignedShort(is)]];
                result.serializable |= SERIALIZALBE_INTERFACE.equals(intName);
                result.tracked |= markerInterface.equals(intName);
            }
        } finally {
            if (is != null)
                is.close();
        }
        return result;
    }

    /**
     * Reads a unsigned short from the stream
     * 
     * @param in The inputstream to use
     * @return A integer of the value
     * @throws IOException If something goes wrong
     */
    private static int readUnsignedShort(final InputStream in) throws IOException {
        return (in.read() << 8) + (in.read() << 0);
    }

    /**
     * Reads a UTF8 String from the stream
     * 
     * @param in The inputstream to use
     * @return A string
     * @throws IOException If something goes wrong
     */
    private static String readUTF(final InputStream in) throws IOException {
        final int utflen = readUnsignedShort(in);
        final byte[] bytearr = new byte[utflen];
        final char[] chararr = new char[utflen];

        int c, char2, char3;
        int count = 0;
        int chararr_count = 0;
        // read the string into the byte array
        while (count < utflen)
            count += in.read(bytearr, chararr_count + count, utflen - count);
        count = 0;

        while (count < utflen) {
            c = bytearr[count] & 0xff;
            if (c > 127)
                break;
            count++;
            chararr[chararr_count++] = (char) c;
        }

        while (count < utflen) {
            c = bytearr[count] & 0xff;
            switch (c >> 4) {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                    /* 0xxxxxxx */
                    count++;
                    chararr[chararr_count++] = (char) c;
                    break;
                case 12:
                case 13:
                    /* 110x xxxx 10xx xxxx */
                    count += 2;
                    char2 = bytearr[count - 1];
                    chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
                    break;
                case 14:
                    /* 1110 xxxx 10xx xxxx 10xx xxxx */
                    count += 3;
                    char2 = bytearr[count - 2];
                    char3 = bytearr[count - 1];
                    chararr[chararr_count++] = (char) (((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
                    break;
                default:
                    /* 10xx xxxx, 1111 xxxx */
                    throw new UTFDataFormatException("malformed input around byte " + count);
            }
        }
        // The number of chars produced may be less than utflen
        return new String(chararr, 0, chararr_count);
    }
}
